package org.etnaframework.module.web.api;

import org.etnaframework.module.base.config.ConstantKeys;
import org.etnaframework.module.base.utils.DbMap;
import org.etnaframework.module.base.utils.HttpClientUtils;
import org.etnaframework.module.base.utils.JacksonUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.util.StreamUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.util.ContentCachingRequestWrapper;
import org.springframework.web.util.ContentCachingResponseWrapper;
import org.springframework.web.util.WebUtils;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.nio.charset.StandardCharsets;
import java.util.*;

/**
 * Web访问日志的生成器，默认实现为生成cURL指令，可在业务代码中按需覆盖
 *
 * @author jasyaf
 * @since 2023-12-27
 */
public class DefaultWebAccessLogCustomizer implements IWebAccessLogCustomizer {

    public static final Logger log = LoggerFactory.getLogger(DefaultWebAccessLogCustomizer.class);

    public DefaultWebAccessLogCustomizer() {
    }

    /**
     * 标记为SLOW的请求处理时间，单位ms，如果想要配置就在业务代码覆盖此方法然后填入想要的值即可
     */
    public long getSlowMs() {
        return 1000;
    }

    /**
     * 请求日志中要记录哪些header，如果为空set就不记录，如果为null就记录所有，如果想要配置就在业务代码覆盖此方法然后填入想要的值即可
     */
    public Set<String> getRequestIncludeHeaders() {
        return null;
    }

    /**
     * 请求日志中的CONTENT最大长度，如果为0表示不限制，如果想要配置就在业务代码覆盖此方法然后填入想要的值即可
     */
    public int getRequestContentMaxLength() {
        return 0;
    }

    /**
     * 返回日志中要记录哪些header，如果为空set就不记录，如果为null就记录所有，如果想要配置就在业务代码覆盖此方法然后填入想要的值即可
     */
    public Set<String> getResponseIncludeHeaders() {
        return null;
    }

    /**
     * 返回日志中的CONTENT最大长度，如果为0表示不限制，如果想要配置就在业务代码覆盖此方法然后填入想要的值即可
     */
    public int getResponseContentMaxLength() {
        return 0;
    }

    @Override
    public boolean needToSaveLog(HttpServletRequest request, HttpServletResponse response, String path) {
        return true;
    }

    @Override
    public String createAccessLog(HttpServletRequest request, HttpServletResponse response, long ts) {
        // 请求结束时间|请求来源IP和端口|HTTP返回码|处理耗时ms|cURL模拟请求命令|请求参数|返回headers|返回Content-Type|返回数据内容
        String format = "%15s:%-5s|%3s|%s| %s |%s|%s|##########| %s";
        Object[] args = {
            request.getRemoteAddr(),
            request.getRemotePort(),
            response.getStatus(),
            // 把返回慢的接口标记出来
            ts + "ms" + (ts > getSlowMs() ? "-SLOW" : ""),
            getRequestContentInCURL(request, getRequestIncludeHeaders(), getRequestContentMaxLength()),
            getResponseHeaders(request, response, getRequestIncludeHeaders()),
            Optional.ofNullable(response.getContentType()).orElse("NoContentType"),
            getResponseContent(request, response, getResponseContentMaxLength())
        };
        return String.format(format, args);
    }

    /**
     * 获取请求信息，以cURL格式输出
     *
     * @param includeHeaders 记录的信息中包含的header，如果为空set就不记录，如果为null就记录所有
     */
    public static String getRequestContentInCURL(HttpServletRequest request, Set<String> includeHeaders, int requestContentMaxLength) {
        StringBuilder r = new StringBuilder();
        r.append("curl -X ").append(request.getMethod());

        String ae = request.getHeader(HttpHeaders.ACCEPT_ENCODING);
        if (StringUtils.hasText(ae) && (ae.contains("gzip") || ae.contains("deflate"))) {
            r.append(" --compressed");
        }

        String parametersEncoded = null; // 用于还原请求URL中?a=1&b=2的部分
        String parametersDecoded = null; // 这里只保存urlEncode之后跟原来不同的，相同的部分就省略了减少日志量

        // 由于request.getParameterMap()是把URL上的参数和POST CONTENT中以form-urlencoded形式传入的参数合并到一起的
        // 没有办法精确地还原请求时的参数究竟是在URL上还是在POST CONTENT，所以这里按下列的规则处理：
        //
        // 如果Content-Type = application/x-www-form-urlencoded则假定所有参数都在POST CONTENT，否则假定所有的参数都在URL
        String contentType = Optional.ofNullable(request.getContentType()).orElse("");
        if (!contentType.contains(MediaType.APPLICATION_FORM_URLENCODED_VALUE)) {
            StringBuilder encoded = new StringBuilder();
            StringBuilder decoded = new StringBuilder();
            Map<String, String[]> parameterMap = request.getParameterMap();
            for (Map.Entry<String, String[]> e : parameterMap.entrySet()) {
                String paramName = e.getKey();
                for (String val : e.getValue()) {
                    String valEncoded = HttpClientUtils.encodeURIComponent(val, StandardCharsets.UTF_8);
                    if (!val.equals(valEncoded)) {
                        decoded.append(paramName).append("=").append(val).append("&");
                    }
                    encoded.append(paramName).append("=").append(valEncoded).append("&");
                }
            }
            parametersEncoded = encoded.length() > 0 ? encoded.subSequence(0, encoded.length() - 1).toString() : "";
            parametersDecoded = decoded.length() > 0 ? decoded.subSequence(0, decoded.length() - 1).toString() : "";
        } else {
            StringBuilder decoded = new StringBuilder();
            Map<String, String[]> parameterMap = request.getParameterMap();
            for (Map.Entry<String, String[]> e : parameterMap.entrySet()) {
                String paramName = e.getKey();
                for (String val : e.getValue()) {
                    String valEncoded = HttpClientUtils.encodeURIComponent(val, StandardCharsets.UTF_8);
                    if (!val.equals(valEncoded)) {
                        decoded.append(paramName).append("=").append(val).append("&");
                    }
                }
            }
            parametersDecoded = decoded.length() > 0 ? decoded.subSequence(0, decoded.length() - 1).toString() : "";
        }

        String host = request.getHeader(HttpHeaders.HOST);
        String scheme = request.getScheme();
        String port = ("http".equals(scheme) && request.getLocalPort() == 80) || ("https".equals(scheme) && request.getLocalPort() == 443) ? "" : ":" + request.getLocalPort();
        r.append(" -v '").append(scheme).append("://");
        r.append(StringUtils.hasText(host) ? host : request.getLocalAddr() + ":" + port);
        r.append(Optional.ofNullable(request.getAttribute(WebUtils.ERROR_REQUEST_URI_ATTRIBUTE)).map(Object::toString).orElse(request.getRequestURI()));
        r.append(StringUtils.hasText(parametersEncoded) ? "?" + parametersEncoded : "");
        r.append("'");

        if (contentType.contains("json") || contentType.contains("xml") || contentType.contains("text") || contentType.contains(MediaType.APPLICATION_FORM_URLENCODED_VALUE)) {
            if (request instanceof ContentCachingRequestWrapper) {
                ContentCachingRequestWrapper requestWrapper = (ContentCachingRequestWrapper) request;
                String bodyString = null;
                try {
                    // 如果POST CONTENT还没有读取过就读取一下，否则404的请求就无法记录POST CONTENT
                    if (requestWrapper.getContentAsByteArray().length == 0) {
                        bodyString = new String(StreamUtils.copyToByteArray(requestWrapper.getInputStream()), requestWrapper.getCharacterEncoding());
                    } else {
                        bodyString = DbMap.truncate(new String(requestWrapper.getContentAsByteArray(), requestWrapper.getCharacterEncoding()), requestContentMaxLength);
                    }
                } catch (Exception e) {
                    log.error("", e);
                }
                if (StringUtils.hasText(bodyString)) {
                    String bodyStringEscaped = DbMap.escapeLineBreak(bodyString);
                    if (bodyString.length() != bodyStringEscaped.length()) {
                        r.append(" --data-binary $'").append(bodyStringEscaped).append("'");
                    } else {
                        r.append(" -d '").append(bodyString).append("'");
                    }
                }
            }
        }

        // 只有指定的header才会被记录下来，缩短记录长度（如果不传配置默认全记录）
        Enumeration<String> headerNames = request.getHeaderNames();
        if (null != headerNames && headerNames.hasMoreElements()) {
            while (headerNames.hasMoreElements()) {
                String name = headerNames.nextElement();
                if (null == includeHeaders || includeHeaders.contains(name)) {
                    Enumeration<String> headerValues = request.getHeaders(name);
                    while (headerValues.hasMoreElements()) {
                        String value = headerValues.nextElement();
                        r.append(" -H '").append(name).append(":").append(value).append("'");
                    }
                }
            }
        }

        if (StringUtils.hasText(parametersDecoded)) {
            r.append("; #DECODED_PARAMS: ").append(parametersDecoded);
        } else {
            r.append("; #END_OF_REQUEST");
        }
        return r.toString();
    }

    /**
     * 获取返回的响应头Response Header
     *
     * @param includeHeaders 记录的信息中包含的header，如果为空set就不记录，如果为null就记录所有
     */
    public static String getResponseHeaders(HttpServletRequest request, HttpServletResponse response, Set<String> includeHeaders) {
        LinkedHashSet<String> headers = new LinkedHashSet<>(response.getHeaderNames());
        if (headers.isEmpty()) {
            return "{}";
        }
        DbMap data = new DbMap();
        for (String header : headers) {
            if (null == includeHeaders || includeHeaders.contains(header)) {
                Collection<String> headerValues = response.getHeaders(header);
                if (headerValues.size() > 1) {
                    data.put(header, headerValues);
                } else if (!headerValues.isEmpty()) {
                    data.put(header, headerValues.toArray(new String[]{})[0]);
                }
            }
        }
        return JacksonUtils.toJson(data);
    }

    /**
     * 获取返回的CONTENT
     */
    public static String getResponseContent(HttpServletRequest request, HttpServletResponse response, int responseContentMaxLength) {
        Object accessLog = request.getAttribute(ConstantKeys.CUSTOM_ACCESS_LOG);
        if (null != accessLog) {
            return "**" + accessLog; // 如果是自定义的日志内容，前面打上**做个标识跟真实的返回内容区分开
        }
        String responseBody = "";
        ContentCachingResponseWrapper wrapper = WebUtils.getNativeResponse(response, ContentCachingResponseWrapper.class);
        if (wrapper != null) {
            try {
                byte[] contentAsByteArray = wrapper.getContentAsByteArray();
                String contentType = Optional.ofNullable(response.getContentType()).orElse("");
                if (contentType.contains("json") || contentType.contains("xml") || contentType.contains("plain")) {
                    responseBody = new String(contentAsByteArray, wrapper.getCharacterEncoding());
                    String responseBodyEscaped = DbMap.escapeLineBreak(responseBody);
                    if (responseBody.length() != responseBodyEscaped.length()) {
                        responseBody = "**echo $'" + responseBodyEscaped + "'";
                    }
                } else {
                    int status = response.getStatus();
                    if (status == HttpStatus.MOVED_PERMANENTLY.value() || status == HttpStatus.FOUND.value()) {
                        responseBody = String.format("**[Redirect-%d] %s", status, response.getHeader(HttpHeaders.LOCATION));
                    } else if (status == HttpStatus.NOT_MODIFIED.value()) {
                        responseBody = String.format("**[NotModified-%d]", status);
                    } else {
                        responseBody = String.format("**[BinaryData(%d)]", contentAsByteArray.length);
                    }
                }
            } catch (Exception e) {
                log.error("", e);
            }
        }
        return DbMap.truncate(responseBody, responseContentMaxLength);
    }
}
